/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

package com.aduib.boot.common.proxy.support;

import com.aduib.boot.common.proxy.MethodInterceptor;
import com.aduib.boot.common.proxy.invocation.CglibMethodInvocation;
import net.sf.cglib.core.ClassGenerator;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.core.DefaultGeneratorStrategy;
import net.sf.cglib.core.DefaultNamingPolicy;
import net.sf.cglib.core.Predicate;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodProxy;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.List;

/**
 * @description: CglibProxy
 * @author: zzh
 * @date: 2021/10/26 15:16
 */
public class CglibProxy extends AbstractProxy {

  private final boolean enabledPrintClassInfo =
      Boolean.parseBoolean(System.getProperty("aduib.class.enabledPrint", "false"));

  public CglibProxy(Object targetObject) {
    super(targetObject);
  }

  public CglibProxy(Class<?> targetClass) {
    super(targetClass);
  }

  @Override
  protected Object doGetProxy(
      ClassLoader classLoader,
      Class<?> targetClass,
      Class<?>[] interfaces,
      Class<?> superClass,
      List<MethodInterceptor> methodInterceptorList) {
    if (enabledPrintClassInfo) {
      setCglibProxyDebugProperties();
    }
    final Enhancer enhancer = createEnhancer();
    enhancer.setClassLoader(classLoader);
    enhancer.setSuperclass(superClass);
    enhancer.setInterfaces(interfaces);
    enhancer.setNamingPolicy(CGLibProxyNamePolicy.INSTANCE);
    enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));
    Callback[] callbacks = getCallbacsk(this);
    Class<?>[] types = new Class<?>[callbacks.length];
    for (int x = 0; x < types.length; x++) {
      types[x] = callbacks[x].getClass();
    }
    enhancer.setCallbackTypes(types);
    return createProxyClassAndInstance(enhancer, callbacks);
  }

  private void setCglibProxyDebugProperties() {
    System.setProperty(
        DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, System.getProperty("user.home"));
  }

  protected Callback[] getCallbacsk(CglibProxy cglibProxy) {
    return new Callback[] {new DynamicMethodInterceptor(cglibProxy)};
  }

  protected Enhancer createEnhancer() {
    return new Enhancer();
  }

  protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
      enhancer.setInterceptDuringConstruction(false);
      enhancer.setCallbacks(callbacks);
      return (constructorArgs != null && constructorArgTypes != null
              ? enhancer.create(constructorArgTypes, constructorArgs)
              : enhancer.create());
  }

  static class CGLibProxyNamePolicy extends DefaultNamingPolicy {

    public static final CGLibProxyNamePolicy INSTANCE = new CGLibProxyNamePolicy();
    public static final String CGLIBGENERATED = "CGLIBGenerated";
    private static final boolean STRESS_HASH_CODE =
        Boolean.getBoolean("net.sf.cglib.test.stressHashCodes");

    @Override
    protected String getTag() {
      return CGLIBGENERATED;
    }

    @Override
    public String getClassName(String prefix, String source, Object key, Predicate names) {
      if (prefix == null) {
        prefix = "net.sf.cglib.empty.Object";
      } else if (prefix.startsWith("java")) {
        prefix = "$$" + prefix;
      }
      String base = "";
      if (source.endsWith("FastClass")) {
        base =
                prefix
                        + "$$"
                        + source.substring(source.lastIndexOf('.') + 1)
                        + "$$"
                        + getTag()
                        + "$$"
                        + Integer.toHexString(STRESS_HASH_CODE ? 0 : key.hashCode());
      } else if (!prefix.contains(getTag())) {
        base =
                prefix
                        + "$$"
                        + getTag()
                        + "$$"
                        + Integer.toHexString(STRESS_HASH_CODE ? 0 : key.hashCode());
      } else {
        base = prefix;
      }
      String attempt = base;
      int index = 2;
      while (names.evaluate(attempt)) {
        attempt = base + "_" + index++;
      }
      return attempt;
    }
  }

  static class ClassLoaderAwareGeneratorStrategy extends DefaultGeneratorStrategy {

    private final ClassLoader classLoader;

    public ClassLoaderAwareGeneratorStrategy(ClassLoader classLoader) {
      this.classLoader = classLoader;
    }

    @Override
    public byte[] generate(ClassGenerator cg) throws Exception {
      if (this.classLoader == null) {
        return super.generate(cg);
      }

      Thread currentThread = Thread.currentThread();
      ClassLoader threadContextClassLoader;
      try {
        threadContextClassLoader = currentThread.getContextClassLoader();
      } catch (Throwable ex) {
        return super.generate(cg);
      }

      boolean overrideClassLoader = !this.classLoader.equals(threadContextClassLoader);
      if (overrideClassLoader) {
        currentThread.setContextClassLoader(this.classLoader);
      }
      try {
        return super.generate(cg);
      } finally {
        if (overrideClassLoader) {
          currentThread.setContextClassLoader(threadContextClassLoader);
        }
      }
    }
  }

  private static class DynamicMethodInterceptor implements net.sf.cglib.proxy.MethodInterceptor, Serializable {

    private final AbstractProxy proxy;

    public DynamicMethodInterceptor(AbstractProxy proxy) {
      this.proxy = proxy;
    }

    @Override
    public Object intercept(Object proxyObj, Method method, Object[] args, MethodProxy methodProxy)
        throws Throwable {
      CglibMethodInvocation invocation =
          new CglibMethodInvocation(
              proxyObj,
              proxy.getSourceObject(),
              method,
              args,
              proxy.getSourceClass(),
              proxy.getAspectList(),
              methodProxy);
      return invocation.proceed();
    }
  }
}
